// ===========================================================================
// CONTENT  : CLASS StringPattern
// AUTHOR   : Manfred Duchrow
// VERSION  : 1.7 - 13/02/2003
// HISTORY  :
//  24/01/2000  duma  CREATED
//  08/01/2002  duma  bugfix  -> Handle *xxx (equal characters after star) correctly
//  16/01/2002  duma  changed -> Implements Serializable
//	06/07/2002	duma	bugfix	-> Couldn't match "London" on "L*n"
//	19/09/2002	duma	bugfix	-> Couldn't match "MA_DR_HRBLUB" on "*_HR*"
//	19/09/2002	duma	changed	-> Using now StringExaminer instead of CharacterIterator
//	29/09/2002	duma	changed	-> Refactored: Using StringExaminer instead of StringScanner
//	26/12/2002	duma	changed	-> Comment of matches() was wrong / new hasWildcard()
//	13/02/2003	duma	added		-> setDigitWildcardChar()
//
// Copyright (c) 2000-2003, by Manfred Duchrow. All rights reserved.
// ===========================================================================
package org.pf.text;

// ===========================================================================
// IMPORTS
// ===========================================================================
import java.io.Serializable;

/**
 * This class provides services for checking strings against string-patterns. Currently it supports the wildcards<br>
 * '*' for any number of any character and <br>
 * '?' for any one character.
 * 
 * The API is very simple:<br>
 * <br>
 * There are only the two class methods <i>match()</i> and <i>matchIgnoreCase()</i>. <br>
 * Example: <br>
 * StringPattern.match( 'Hello World", "H* W*" ) ; --> evaluates to true <br>
 * StringPattern.matchIgnoreCase( 'StringPattern", "str???pat*" ) ; --> evaluates to true <br>
 * 
 * 
 * @author Manfred Duchrow
 * @version 1.7
 */
public class StringPattern implements Serializable {
    // =========================================================================
    // CONSTANTS
    // =========================================================================
    protected final static String MULTI_WILDCARD = "*";
    protected final static char MULTICHAR_WILDCARD = '*';
    protected final static char SINGLECHAR_WILDCARD = '?';

    // =========================================================================
    // INSTANCE VARIABLES
    // =========================================================================
    private boolean ignoreCase = false;

    /**
     * Returns whether or not the pattern matching ignores upper and lower case
     */
    public boolean getIgnoreCase() {
        return ignoreCase;
    }

    /**
     * Sets whether the pattern matching should ignore case or not
     */
    public void setIgnoreCase(boolean newValue) {
        ignoreCase = newValue;
    }

    private String pattern = null;

    /**
     * Returns the pattern as string.
     */
    public String getPattern() {
        return pattern;
    }

    /**
     * Sets the pattern to a new value
     */
    public void setPattern(String newValue) {
        pattern = newValue;
    }

    // -------------------------------------------------------------------------

    private Character digitWildcard = null;

    protected Character digitWildcard() {
        return digitWildcard;
    }

    protected void digitWildcard(Character newValue) {
        digitWildcard = newValue;
    }

    // =========================================================================
    // CLASS METHODS
    // =========================================================================
    /**
     * Returns true, if the given probe string matches the given pattern. <br>
     * The character comparison is done case sensitive.
     * 
     * @param probe
     *            The string to check against the pattern.
     * @param pattern
     *            The patter, that probably contains wildcards ( '*' or '?' )
     */
    public static boolean match(String probe, String pattern) {
        StringPattern stringPattern = new StringPattern(pattern, false);
        return (stringPattern.matches(probe));
    } // match()

    // -------------------------------------------------------------------------

    /**
     * Returns true, if the given probe string matches the given pattern. <br>
     * The character comparison is done ignoring upper/lower-case.
     * 
     * @param probe
     *            The string to check against the pattern.
     * @param pattern
     *            The patter, that probably contains wildcards ( '*' or '?' )
     */
    public static boolean matchIgnoreCase(String probe, String pattern) {
        StringPattern stringPattern = new StringPattern(pattern, true);
        return (stringPattern.matches(probe));
    } // matchIgnoreCase()

    // -------------------------------------------------------------------------

    // =========================================================================
    // CONSTRUCTORS
    // =========================================================================
    /**
     * Initializes the new instance with the string pattern and the selecteion, if case should be ignored when comparing characters.
     * 
     * @param pattern
     *            The pattern to check against ( May contain '*' and '?' wildcards )
     * @param ignoreCase
     *            Definition, if case sensitive character comparison or not.
     */
    public StringPattern(String pattern, boolean ignoreCase) {
        this.setPattern(pattern);
        this.setIgnoreCase(ignoreCase);
    } // StringPattern()

    // -------------------------------------------------------------------------

    /**
     * Initializes the new instance with the string pattern. The default is case sensitive checking.
     * 
     * @param pattern
     *            The pattern to check against ( May contain '*' and '?' wildcards )
     */
    public StringPattern(String pattern) {
        this(pattern, false);
    } // StringPattern()

    // -------------------------------------------------------------------------

    /**
     * Initializes the new instance with the string pattern and a digit wildcard character. The default is case sensitive checking.
     * 
     * @param pattern
     *            The pattern to check against ( May contain '*', '?' wildcards and the digit wildcard )
     * @param digitWildcard
     *            A wildcard character that stands as placeholder for digits
     */
    public StringPattern(String pattern, char digitWildcard) {
        this(pattern, false, digitWildcard);
    } // StringPattern()

    // -------------------------------------------------------------------------

    /**
     * Initializes the new instance with the string pattern and the selecteion, if case should be ignored when comparing characters plus a wildcard
     * character for digits.
     * 
     * @param pattern
     *            The pattern to check against ( May contain '*' and '?' wildcards )
     * @param ignoreCase
     *            Definition, if case sensitive character comparison or not.
     * @param digitWildcard
     *            A wildcard character that stands as placeholder for digits
     */
    public StringPattern(String pattern, boolean ignoreCase, char digitWildcard) {
        this.setPattern(pattern);
        this.setIgnoreCase(ignoreCase);
        this.setDigitWildcardChar(digitWildcard);
    } // StringPattern()

    // -------------------------------------------------------------------------

    // =========================================================================
    // PUBLIC INSTANCE METHODS
    // =========================================================================

    /**
     * Tests if a specified string matches the pattern.
     * 
     * @param probe
     *            The string to compare to the pattern
     * @return true if and only if the probe matches the pattern, false otherwise.
     */
    public boolean matches(String probe) {
        StringExaminer patternIterator = null;
        StringExaminer probeIterator = null;
        char patternCh = '-';
        char probeCh = '-';
        String newPattern = null;
        String subPattern = null;
        int charIndex = 0;

        if (probe == null)
            return false;
        if (probe.length() == 0)
            return false;

        patternIterator = this.newExaminer(this.getPattern());
        probeIterator = this.newExaminer(probe);

        probeCh = probeIterator.nextChar();
        patternCh = this.getPatternChar(patternIterator, probeCh);

        while ((this.endNotReached(patternCh)) && (this.endNotReached(probeCh))) {

            if (patternCh == MULTICHAR_WILDCARD) {
                patternCh = this.skipWildcards(patternIterator);
                if (this.endReached(patternCh)) {
                    return true; // No more characters after multi wildcard - So everything matches
                } else {
                    patternIterator.skip(-1);
                    newPattern = this.upToEnd(patternIterator);
                    charIndex = newPattern.indexOf(MULTICHAR_WILDCARD);
                    if (charIndex >= 0) {
                        subPattern = newPattern.substring(0, charIndex);

                        if (this.skipAfter(probeIterator, subPattern)) {
                            patternIterator = this.newExaminer(newPattern.substring(charIndex));
                            patternCh = probeCh;
                        } else {
                            return false;
                        }
                    } else {
                        probeIterator.skip(-1);
                        return this.matchReverse(newPattern, probeIterator);
                    }
                }
            }

            if (this.charsAreEqual(probeCh, patternCh)) {
                if (this.endNotReached(patternCh)) {
                    probeCh = probeIterator.nextChar();
                    patternCh = this.getPatternChar(patternIterator, probeCh);
                }
            } else {
                if (patternCh != MULTICHAR_WILDCARD)
                    return false; // character is not matching - return immediately
            }
        } // while()

        return ((this.endReached(patternCh)) && (this.endReached(probeCh)));
    } // matches()

    // -------------------------------------------------------------------------

    /**
     * Returns the pattern string.
     * 
     * @see java.lang.Object#toString()
     */
    public String toString() {
        if (this.getPattern() == null)
            return super.toString();
        else
            return this.getPattern();
    } // toString()

    // -------------------------------------------------------------------------

    /**
     * Returns true if the pattern contains any '*' or '?' wildcard character.
     */
    public boolean hasWildcard() {
        if (this.getPattern() == null)
            return false;

        if (this.hasDigitWildcard()) {
            if (this.getPattern().indexOf(this.digitWildcardChar()) >= 0)
                return true;
        }

        return (this.getPattern().indexOf(MULTI_WILDCARD) >= 0) || (this.getPattern().indexOf(SINGLECHAR_WILDCARD) >= 0);
    } // hasWildcard()

    // -------------------------------------------------------------------------

    /**
     * Sets the given character as a wildcard character in this pattern to match only digits ('0'-'9'). <br>
     * 
     * @param digitWildcard
     *            The placeholder character for digits
     */
    public void setDigitWildcardChar(char digitWildcard) {
        if (digitWildcard <= 0) {
            this.digitWildcard(null);
        } else {
            this.digitWildcard(new Character(digitWildcard));
        }
    } // setDigitWildcardChar()

    // -------------------------------------------------------------------------

    // =========================================================================
    // PROTECTED INSTANCE METHODS
    // =========================================================================
    protected boolean hasDigitWildcard() {
        return this.digitWildcard() != null;
    } // hasDigitWildcard()

    // -------------------------------------------------------------------------

    protected char digitWildcardChar() {
        if (this.hasDigitWildcard())
            return this.digitWildcard().charValue();
        else
            return '\0';
    } // digitWildcardChar()

    // -------------------------------------------------------------------------

    /**
     * Moves the iterator position to the next character that is no wildcard. Doesn't skip digit wildcards !
     */
    protected char skipWildcards(StringExaminer iterator) {
        char result = '-';

        do {
            result = iterator.nextChar();
        }
        while ((result == MULTICHAR_WILDCARD) || (result == SINGLECHAR_WILDCARD));
        return result;
    } // skipWildcards()

    // -------------------------------------------------------------------------

    /**
     * Increments the given iterator up to the last character that matched the character sequence in the given matchString. Returns true, if the
     * matchString was found, otherwise false.
     * 
     * @param matchString
     *            The string to be found (must not contain *)
     */
    protected boolean skipAfter(StringExaminer examiner, String matchString) {
        // Do not use the method of StringExaminer anymore, because digit wildcard
        // support is in the charsAreEqual() method which is unknown to the examiner.
        // return examiner.skipAfter( matchString ) ;

        char ch = '-';
        char matchChar = ' ';
        boolean found = false;
        int index = 0;

        if ((matchString == null) || (matchString.length() == 0))
            return false;

        ch = examiner.nextChar();
        while ((examiner.endNotReached(ch)) && (!found)) {
            matchChar = matchString.charAt(index);
            if (this.charsAreEqual(ch, matchChar)) {
                index++;
                if (index >= matchString.length()) // whole matchString checked ?
                {
                    found = true;
                } else {
                    ch = examiner.nextChar();
                }
            } else {
                if (index == 0) {
                    ch = examiner.nextChar();
                } else {
                    index = 0;
                }
            }
        }
        return found;
    } // skipAfter()

    // -------------------------------------------------------------------------

    protected String upToEnd(StringExaminer iterator) {
        return iterator.upToEnd();
    } // upToEnd()

    // -------------------------------------------------------------------------

    protected boolean matchReverse(String pattern, StringExaminer probeIterator) {
        String newPattern;
        String newProbe;
        StringPattern newMatcher;

        newPattern = MULTI_WILDCARD + pattern;
        newProbe = this.upToEnd(probeIterator);
        newPattern = this.strUtil().reverse(newPattern);
        newProbe = this.strUtil().reverse(newProbe);
        newMatcher = new StringPattern(newPattern, this.getIgnoreCase());
        if (this.hasDigitWildcard())
            newMatcher.setDigitWildcardChar(this.digitWildcardChar());

        return newMatcher.matches(newProbe);
    } // matchReverse()

    // -------------------------------------------------------------------------

    protected boolean charsAreEqual(char probeChar, char patternChar) {
        if (this.hasDigitWildcard()) {
            if (patternChar == this.digitWildcardChar()) {
                return Character.isDigit(probeChar);
            }
        }

        if (this.getIgnoreCase()) {
            return (Character.toUpperCase(probeChar) == Character.toUpperCase(patternChar));
        } else {
            return (probeChar == patternChar);
        }
    } // charsAreEqual()

    // -------------------------------------------------------------------------

    protected boolean endReached(char character) {
        return (character == StringExaminer.END_REACHED);
    } // endReached()

    // -------------------------------------------------------------------------

    protected boolean endNotReached(char character) {
        return (!endReached(character));
    } // endNotReached()

    // -------------------------------------------------------------------------

    protected char getPatternChar(StringExaminer patternIterator, char probeCh) {
        char patternCh;

        patternCh = patternIterator.nextChar();

        return ((patternCh == SINGLECHAR_WILDCARD) ? probeCh : patternCh);
    } // getPatternChar()

    // -------------------------------------------------------------------------

    protected StringExaminer newExaminer(String str) {
        return new StringExaminer(str, this.getIgnoreCase());
    } // newExaminer()

    // -------------------------------------------------------------------------

    protected StringUtil strUtil() {
        return StringUtil.current();
    } // strUtil()

    // -------------------------------------------------------------------------

} // class StringPattern